package moe.dao.pool;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;

import moe.config.Configuration;

public class ConnectionManager {
	private final static int CORE_CONNECTION_SIZE = 2;
	private final static int MAX_CONNECTION_SIZE = 10;
	private final static int CONNECTION_ALIVE_TIMEOUT = 10 * 60 * 1000;
	private final static int CORE_CONNECTION_ALIVE_TIMEOUT = 3 * 60 * 60 * 1000;
	private final static int CHECKOUT_TIMEOUT = 5 * 1000;

	private static final Logger log = Logger.getLogger(ConnectionManager.class.getName());

	private static final Object instanceLock = new Object();
	private static ConnectionManager defaultInstance, dumpInstance, collectionInstance;

	private final Lock connectionLock = new ReentrantLock();
	private final ConnectionBuilder builder;
	private final Pool<Connection> pool;
	private int connectionCount = 0;

	public static ConnectionManager getDumpManager() {
		synchronized (instanceLock) {
			if (dumpInstance == null) {
				String driverClass = "org.sqlite.JDBC";
				String jdbcUrl = "jdbc:sqlite:./moe.s3db";

				dumpInstance = new ConnectionManager(driverClass, jdbcUrl, "", "");
			}
		}
		return dumpInstance;
	}

	public static ConnectionManager getCollectionManager() {
		synchronized (instanceLock) {
			if (collectionInstance == null) {
				String driverClass = "org.sqlite.JDBC";
				String jdbcUrl = "jdbc:sqlite:./collection.s3db";

				collectionInstance = new ConnectionManager(driverClass, jdbcUrl, "", "");
			}
		}
		return collectionInstance;
	}

	public static ConnectionManager getDefaultManager() {
		synchronized (instanceLock) {
			if (defaultInstance == null) {
				String driverClass = Configuration.getDriverClass();
				String jdbcUrl = Configuration.getJdbcUrl();
				String user = Configuration.getDbUser();
				String password = Configuration.getDbPassword();

				defaultInstance = new ConnectionManager(driverClass, jdbcUrl, user, password);
			}
		}
		return defaultInstance;
	}

	private ConnectionManager(String driverClass, String jdbcUrl, String user, String password) {
		builder = new ConnectionBuilder(driverClass, jdbcUrl, user, password);
		pool = new Pool<Connection>(MAX_CONNECTION_SIZE, CORE_CONNECTION_SIZE,
				CONNECTION_ALIVE_TIMEOUT,
				CORE_CONNECTION_ALIVE_TIMEOUT);
	}

	public Connection getConnection() throws SQLException {
		Connection conn = null;
		connectionLock.lock();
		try {
			if (!pool.isEmpty()) {
				conn = getConnectionFromPool(false);
			}
			if (conn == null) {
				if (connectionCount < MAX_CONNECTION_SIZE) {
					conn = builder.createConnection(pool);
					connectionCount++;
				} else {
					conn = getConnectionFromPool(true);
				}
			} else {
				if (log.isLoggable(Level.FINE)) {
					log.fine(conn.toString() + "\n\tbingo connection pool, size:" + pool.size()
							+ ", count:" + connectionCount);
				}
			}
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		} finally {
			connectionLock.unlock();
		}
		return conn;
	}

	public PreparedStatement getUncachePreparedStatement(String sql, Connection connection)
			throws SQLException {
		Connection principalConnection = builder.getPrincipalConnection(connection);
		if (connection == null) {
			throw new SQLException("Can't got the principal connection");
		}
		Statement principalStatement = principalConnection.prepareStatement(sql);
		InvocationHandler handler = new UncachePreparedStatementInvocationHandler(
				principalStatement, connection);
		return (PreparedStatement) Proxy.newProxyInstance(
				getClass().getClassLoader(),
				new Class[] { PreparedStatement.class },
				handler);
	}

	private Connection getConnectionFromPool(boolean isWait) throws InterruptedException,
			SQLException {
		Connection conn = null;
		PoolEntity<Connection> entity = null;
		while (!pool.isEmpty() || isWait) {
			entity = pool.checkout(CHECKOUT_TIMEOUT, TimeUnit.MILLISECONDS);
			if (entity == null) {
				throw new SQLException("Get database connection times out");
			}
			conn = entity.get();
			if (!entity.isTimeout() && !conn.isClosed()) {
				break;
			}
			connectionCount--;
		}
		return conn;
	}

	public void close() {
		connectionLock.lock();
		try {
			pool.clear();
			if (this == defaultInstance) {
				defaultInstance = null;
			} else if (this == collectionInstance) {
				collectionInstance = null;
			} else if (this == dumpInstance) {
				dumpInstance = null;
			}
		} finally {
			connectionLock.unlock();
		}
	}

	public static void closeAll() {
		if (defaultInstance != null) {
			defaultInstance.close();
		}
		if (dumpInstance != null) {
			dumpInstance.close();
		}
		if (collectionInstance != null) {
			collectionInstance.close();
		}
	}

	private static String escapeChar = null;

	public String getSearchStringEscape() {
		if (escapeChar == null) {
			try {
				Connection conn = this.getConnection();
				DatabaseMetaData metaData = conn.getMetaData();
				escapeChar = metaData.getSearchStringEscape();
				conn.close();
			} catch (SQLException e) {
				throw new RuntimeException(e);
			}
			if (escapeChar == null) {
				escapeChar = "\\";
			}
		}
		return escapeChar;
	}

	private static class UncachePreparedStatementInvocationHandler implements InvocationHandler {
		private Statement principal;
		private Connection proxyConnection;

		public UncachePreparedStatementInvocationHandler(Statement stat, Connection proxy) {
			principal = stat;
			proxyConnection = proxy;
		}

		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			Object result = null;
			if ("getConnection".equals(method.getName())) {
				method.invoke(principal, args);
				result = proxyConnection;
			} else {
				result = method.invoke(principal, args);
			}
			return result;
		}

	}
}
